package tp.badugi.server;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;

public class GamePlayController {
	
	private static final int ACTION_REQUEST_TIMEOUT = 30000;
	private static final int CARD_REQUEST_TIMEOUT = 30000;
	private static final int SMALL_BLIND_POSITION = 0;
	private static final int BIG_BLIND_POSITION = 1;
	private static final int DELAY_AFTER_DEALING_CARDS = 3000;
	 
	/**
	 * Players involved in the game.
	 */
	private List<Player> players;
	
	/**
	 * Players that remain in the game (haven't folded).
	 */
	private List<Player> remainingPlayers;
	
	/**
	 * Players that are still able to perform actions (haven't folded or gone all-in).
	 */
	private List<Player> actingPlayers;
	
	private Deck deck;
	private Table table;
	private int bettingRound;
	private Player playerInAction;
	
	public GamePlayController(Table tableController) {
		this.table = tableController;
		deck = new Deck();
	}

	public int getBettingRound() {
		return bettingRound;
	}

	public Player getPlayerInAction() {
		return playerInAction;
	}

	public void overseeDeal(List<Player> nonBankruptPlayers, int playerWithButton) 
			throws InterruptedException 
	{
		// Internally rearrange player list, if it doesn't start with the small blind
		// This makes keeping track of playing order easier
		players = new ArrayList<Player>(nonBankruptPlayers);
		while ((playerWithButton + 1) % players.size() != 0) {
			players.add(0, players.remove(players.size() - 1));
			playerWithButton++;
		}
		
		// Create a separate list to keep track of who is still able to act and remains in the game
		actingPlayers = new ArrayList<Player>(players);
		remainingPlayers = new ArrayList<Player>(players);
		
		// Game setup
		deck.shuffle();
		getSmallAndBigBlinds();
		dealCards();
		Thread.sleep(DELAY_AFTER_DEALING_CARDS);
		
		// Betting rounds one to three
		for (bettingRound = 1; bettingRound < 4 && actingPlayers.size() > 1; bettingRound++) {
			table.announceRoundChange(bettingRound);
			overseeBettingRound(bettingRound);
			if (actingPlayers.size() > 1) {
				overseeCardExchange();
			}
		}
		
		// Last betting round
		if (actingPlayers.size() > 1) {
			table.announceRoundChange(bettingRound);
			overseeBettingRound(bettingRound);
		}
		
		// Showdown
		initiateShowdown();
	}

	private boolean allPlayersHadChanceToAct(List<Boolean> playerActionBooleanMap) {
		// If all but one player folded then everyone had a chance to act
		if (remainingPlayers.size() <= 1) {
			return true;
		}
		
		// If more then player remains then see if everyone had a chance to act
		boolean allPlayersHadChanceToAct = true;
		for (Boolean thatPlayerHadChance : playerActionBooleanMap) {
			if (thatPlayerHadChance == false) {
				allPlayersHadChanceToAct = false;
			}
		}
		return allPlayersHadChanceToAct;
	}

	private void dealCards() {
		for (Player player : players) {
			List<Card> playersCards = new ArrayList<Card>();
			for (int i = 1; i <= 4; i++) {
				playersCards.add(deck.drawCard());
			}
			player.setCards(playersCards);
			// TODO announce to other players which player received cards
		}
	}

	private void getSmallAndBigBlinds() {
		Player smallBlindPlayer = players.get(SMALL_BLIND_POSITION);
		smallBlindPlayer.placeSmallBlind();
		Action.BET.amount = table.getSmallBlind();
		table.announcePlayersAction(smallBlindPlayer, Action.BET);
		Player bigBlindPlayer = players.get(BIG_BLIND_POSITION);
		Action.BET.amount = table.getBigBlind();
		bigBlindPlayer.placeBigBlind();
		table.announcePlayersAction(bigBlindPlayer, Action.BET);
		
		// If the player has no more chips after placing the big blind then he has gone all in
		if (bigBlindPlayer.getChipCount() == 0) {
			actingPlayers.remove(bigBlindPlayer);
		}
	}

	private void initiateShowdown() {
		
		// Create list of players still remaining in the game (haven't folded)
		List<Player> remainingPlayers = new ArrayList<Player>();
		for (Player player : players) {
			if (player.isInPlay()) {
				remainingPlayers.add(player);
			}
		}
		
		// Determine winner/winners
		if (remainingPlayers.size() > 1) {
			BadugiWinnerResolver winnerResolver = 
					new BadugiWinnerResolver(remainingPlayers, table.getPotSize());
			List<Player> winners = winnerResolver.getWinners();
			Map<Player, Long> potDivision = winnerResolver.getPotDivision();
			
			// Award chips
			for (Player player : remainingPlayers) {
				long amountWon = potDivision.get(player);
				player.awardChips(amountWon);
			}
			
			// Announce winner/winners
			table.announceWinnersRevealed(winners);
		}
		
		// Remaining player takes all
		else {
			Player winningPlayer = remainingPlayers.get(0);
			List<Player> winners = new ArrayList<Player>();
			winners.add(winningPlayer);
			
			// Award pot
			winningPlayer.awardChips(table.getPotSize());
			
			// Don't show cards
			winningPlayer.discardCards();
			
			// Announce winner
			table.announceWinnersRevealed(winners);
		}
		
		// Clear table
		table.clear();
	}

	private void overseeBettingRound(int bettingRound) {
		int startingPlayerPosition;
		int actingPlayerPosition;
		List<Boolean> playerActionBooleanMap;
		EnumSet<Action> aggresiveActions = EnumSet.of(Action.BET, Action.RAISE, Action.ALL_IN);
		
		if (actingPlayers.size() > 1) {
			// Determine who should start the betting round.
			if (bettingRound == 1) {
				startingPlayerPosition = (BIG_BLIND_POSITION + 1) % actingPlayers.size();
			}
			else {
				startingPlayerPosition = SMALL_BLIND_POSITION; 
			}
			
			// Keep track of who had a chance to act.
			playerActionBooleanMap = new ArrayList<Boolean>();
			for (int i = 0; i < actingPlayers.size(); i++) {
				playerActionBooleanMap.add(false);
			}
			
			// Oversee the betting round
			actingPlayerPosition = startingPlayerPosition;
			int raiseCount = 0;
			while (!allPlayersHadChanceToAct(playerActionBooleanMap)) {
				
				// Announce new player in action
				playerInAction = actingPlayers.get(actingPlayerPosition);
				table.announceNewPlayerInAction(playerInAction);
				
				// Request action
				Action chosenAction = 
						playerInAction.requestAction(raiseCount >= 4, ACTION_REQUEST_TIMEOUT);
				playerActionBooleanMap.set(actingPlayerPosition, true);
				table.announcePlayersAction(playerInAction, chosenAction);
				
				// If the player folded then remove him from list of players remaining in game
				if (chosenAction.equals(Action.FOLD)) {
					remainingPlayers.remove(actingPlayerPosition);
				}
				
				// Keep count of how many times the bet was raise
				if (chosenAction.equals(Action.RAISE)) {
					raiseCount++;
				}
				
				// If the player made an aggressive play raise bet amount
				if (aggresiveActions.contains(chosenAction)) {
					table.setBetAmount(playerInAction.getPotContribution());
					
					// All other players get a chance to act again
					for (int i = 0; i < playerActionBooleanMap.size(); i++) {
						if (i != actingPlayerPosition)
							playerActionBooleanMap.set(i, false);
					}
				}
				
				// Determine next player to act
				if (chosenAction.equals(Action.FOLD) || chosenAction.equals(Action.ALL_IN)) {
					// If the player folded or went all-in then exclude him from further betting
					actingPlayers.remove(actingPlayerPosition);
					playerActionBooleanMap.remove(actingPlayerPosition);
					if (actingPlayerPosition >= actingPlayers.size()) {
						actingPlayerPosition = 0;
					}
				}
				else if (actingPlayerPosition + 1 >= actingPlayers.size()) {
					actingPlayerPosition = 0;
				}
				else {
					actingPlayerPosition++;
				}
			}
			
			playerInAction = null;
		}
	}

	private void overseeCardExchange() {
		for (Player player : actingPlayers) {
			playerInAction = player;
			table.announceNewPlayerInAction(playerInAction);
			EnumSet<Card> chosenCards = player.requestCards(CARD_REQUEST_TIMEOUT);
			table.announcePlayerReplacedCards(player, chosenCards == null ? 0 : chosenCards.size());
			if (chosenCards != null && !chosenCards.isEmpty()) {
				List<Card> playersCards = player.getCards();
				for (Card card : chosenCards) {
					int cardIndex = playersCards.indexOf(card);
					if (cardIndex != -1) {
						Card newCard = deck.exchangeCard(card);
						playersCards.set(cardIndex, newCard);
					}
				}
				player.setCards(playersCards);
			}
		}
		
		playerInAction = null;
	}

}
